package com.qianxin.ids.idam.am.authn.factors.cass.upsso;

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.gitee.starblues.realize.PluginUtils;
import com.qianxin.ids.idam.am.authn.api.context.IContextService;
import com.qianxin.ids.idam.am.authn.api.context.SecurityContextHolder;
import com.qianxin.ids.idam.am.authn.api.entity.UpAuthnParam;
import com.qianxin.ids.idam.am.authn.api.factors.*;
import com.qianxin.ids.idam.am.authn.api.iface.IAuthnParam;
import com.qianxin.ids.idam.am.authn.api.iface.IErrorCodeAmAuthn;
import com.qianxin.ids.idam.am.authn.api.iface.IFactorParserService;
import com.qianxin.ids.idam.am.authn.api.utils.AuthnFactorInstanceUtils;
import com.qianxin.ids.idam.am.authn.factors.cass.upsso.api.CassCommon;
import com.qianxin.ids.idam.am.authn.factors.cass.upsso.api.CassUpSsoProperties;
import com.qianxin.ids.idam.am.authn.factors.cass.upsso.refreshToken.RefreshEntity;
import com.qianxin.ids.idam.am.authn.factors.cass.upsso.refreshToken.RefreshTokenCache;
import com.qianxin.ids.idam.am.authn.factors.common.strategy.FactorParserStrategy;
import com.qianxin.ids.idam.am.common.api.entity.SSOProtocol;
import com.qianxin.ids.idam.am.session.api.adapter.SessionAdapterFactory;
import com.qianxin.ids.idam.am.session.api.entity.AuthnContext;
import com.qianxin.ids.idam.framework.common.exception.IErrorCodeBase;
import com.qianxin.ids.idam.framework.common.rsa.RsaServiceAdapterFactory;
import com.qianxin.ids.idam.sdk.exception.CodedException;
import com.qianxin.ids.idam.sdk.utils.StringUtils;
import com.qianxin.ids.idam.sdk.utils.encrypt.RsaUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.utils.URLEncodedUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClientException;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.HttpCookie;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;


/**
 * @author xiongjianlin
 */
@Slf4j
@Component
public class PluginCassUpSsoAuthnFactorImpl extends AbstractAuthnFactor<UpAuthnParam> {

    private static final String AUTHN_TYPE = "CASS_UP_SSO";
    private static final String IV = "fiyme84mairygdwl";
    private static final String PARSER_TYPE = "default";

    private FactorParserStrategy factorParserStrategy;
    private RsaServiceAdapterFactory rsaServiceAdapterFactory;
    private IContextService contextService;

    private SessionAdapterFactory sessionAdapterFactory;

    @Autowired
    private PluginUtils pluginUtils;
    @Autowired
    RefreshTokenCache cache;

    @PostConstruct
    private void init() {
        factorParserStrategy = pluginUtils.getMainBean(FactorParserStrategy.class);
        contextService = pluginUtils.getMainBean(IContextService.class);
        rsaServiceAdapterFactory = pluginUtils.getMainBean(RsaServiceAdapterFactory.class);
        sessionAdapterFactory = pluginUtils.getMainBean(SessionAdapterFactory.class);
    }


    @Override
    protected AuthnType initAuthnType() {
        return AuthnType.builder()
                .type(AUTHN_TYPE)
                .mainAuthn(AuthnTypeEnum.MAIN_ONLY)
                .uitype(AuthnFactorUitypeEnum.USER_PWD)
                .level(1)
                .i18n("{label:{'zh':'中国社会科学院UP认证','en':'CASS UP Certification'}}")
                .label("中国社会科学院UP认证")
                .singleton(false)
                .endtype(this.initEndType())
                .build();
    }

    private List<AuthnFactorEndtypeEnum> initEndType() {
        List<AuthnFactorEndtypeEnum> endTypeList = new ArrayList<>();
        endTypeList.add(AuthnFactorEndtypeEnum.PC);
        endTypeList.add(AuthnFactorEndtypeEnum.Web);
        endTypeList.add(AuthnFactorEndtypeEnum.Mobile);
        return endTypeList;
    }

    @Override
    protected AuthnFactorResponse<String> doAuthnInner(IAuthnParam authnParam, AuthnFactorInstance authnFactorInstance, HttpServletRequest request, String authnContextId) throws CodedException {
        UpAuthnParam param = (UpAuthnParam) authnParam;
        String pwd = RsaUtil.decrypt(param.getCredential(), rsaServiceAdapterFactory.getRsaService().getPrivateKey());
        String user = param.getPrincipal();
        CassUpSsoProperties properties = CassUpSsoProperties.parse(authnFactorInstance);

        String authnUrl = properties.getAuthnUrl();
        String aesKey = properties.getAesKey();

        // 加密账户、密码
        AES aes = new AES(Mode.CBC, Padding.ZeroPadding, aesKey.getBytes(), IV.getBytes());

        byte[] aesUser = aes.encrypt(user);
        byte[] aesPwd = aes.encrypt(pwd);
        log.info("AES 加密后，用户【{}】，密码【{}】", aesUser, aesPwd);
        String baseUser = Base64.encode(aesUser);
        String basePwd = Base64.encode(aesPwd);
        log.info("base64 编码后，用户【{}】，密码【{}】", baseUser, basePwd);
        HashMap<String, Object> postParam = new HashMap<>();
        postParam.put("username", baseUser);
        postParam.put("password", basePwd);
        HttpResponse execute = HttpRequest.post(authnUrl)
                .header(Header.CONTENT_TYPE, URLEncodedUtils.CONTENT_TYPE)
                .header("Accept", "application/json")
                .form(postParam)
                .timeout(5000)
                .execute();
        String body = execute.body();
        log.info("第三方返回值execute为：{}", execute);
        log.info("第三方返回值内容为：{}", body);
        /**
         * 正确返回
         * {
         *     "status": 0,
         *     "msg": "ok",
         *     "data": {
         *         "data": {
         *             "token": "bd0edad2-2946-48d6-8880-a958f8e9a4f8",
         *             "parameterName": "_csrf",
         *             "headerName": "X-CSRF-TOKEN"
         *         },
         *         "url": "/system/chooseRole",
         *         "userToken": null,
         *         "urgeChar": null
         *     }
         * }
         * <p>
         * 错误返回
         * {
         *     "timestamp": 1724719474184,
         *     "status": 401,
         *     "error": "Unauthorized",
         *     "message": "用户名或密码错误",
         *     "path": "/loginerror",
         *     "suspected.attack": 1
         * }
         */

        if (StringUtils.isBlank(body)) {
            throw new CodedException(IErrorCodeBase.IAM_SERVER_DATA_NULL_ERROR);
        }
        JSONObject resultBody = JSONObject.parseObject(body);
        if (0 != resultBody.getInteger("status")) {
            log.error("请求失败，返回的消息为：{}", resultBody.getString("message"));
            throw new CodedException(IErrorCodeAmAuthn.AUTH_FAILED, resultBody.getString("message"));
        }
        /**
         * :{“mobileNo”:”1305108945”,”userEmail”:”shanhang@air.com”,”loginName”:”00000”}
         * loginName 作为主键
         */
        String token = resultBody.getJSONObject("data").getJSONObject("data").getString("token");
        // 获取 Cookie
        HttpCookie session = execute.getCookie("SESSION");
        log.info("获取登陆后的cookies 为 ：[{}], session值为: [{}]" , execute.getCookies(), session);
        AuthnContext context = contextService.getAuthnContextByRequest();
        context.setAttribute(CassCommon.TOKEN, token);
        context.setAttribute(CassCommon.TOKEN, token);
        // 保存过期时间
        context.setAttribute(CassCommon.EXPIRETIME, System.currentTimeMillis() + properties.getTokenExpireTime() * 60 * 1000L);
        // todo MOCK 桩不支持 设置COOKIE， 此处先写死值
        // 存入登陆时返回的session值
        context.setAttribute(CassCommon.SESSION, session.getValue());
//        context.setAttribute(CassCommon.SESSION, "123456");
        context.setAttribute(CassCommon.USER, user);
        context.setAttribute(CassCommon.INSTANCE, authnParam.getInstanceId());
        // 存入缓存。
        saveCache(context);
        // 由于刷新用户token需要提前确定岗位信息，因此先调用此方法防止自动任务续期token失败
        log.info("认证完成后获取岗位等信息：" );
        buildExtUrlParam(null);
        log.info("认证完成后获取岗位等信息完成" );
        //设置返回信息
        AuthnFactorResponse<String> response = new AuthnFactorResponse<>();
        response.setUserInfo(user);
        response.setAccount(user);
        response.setSuccess(true);
        return response;
    }

    private void saveCache(AuthnContext context){
        cache.put(new RefreshEntity(context));
    }

    @Override
    public UpAuthnParam getAuthnParam(HttpServletRequest request) {
        UpAuthnParam upAuthnParam = new UpAuthnParam();
        upAuthnParam.setPrincipal(request.getParameter(UpAuthnParam.PRINCIPAL));
        return upAuthnParam;
    }

    @Override
    public UpAuthnParam getAuthnParam(JSONObject request) {
        return request.toJavaObject(UpAuthnParam.class);
    }

    @Override
    public IFactorParserService getUserInfoParser() {
        return factorParserStrategy.getParser(PARSER_TYPE);
    }

    @Override
    public String buildExtUrlParam(JSONObject json) {
        log.debug("buildExtUrlParam 输出json ： 【{}】", json);
        String token = contextService.getAuthnContextByRequest().getAttribute(CassCommon.TOKEN);
        String user = contextService.getAuthnContextByRequest().getAttribute(CassCommon.USER);
        String instanceId = contextService.getAuthnContextByRequest().getAttribute(CassCommon.INSTANCE);
        String session = contextService.getAuthnContextByRequest().getAttribute(CassCommon.SESSION);
        String userToken = contextService.getAuthnContextByRequest().getAttribute(CassCommon.USER_TOKEN);
        log.info("buildExtUrlParam 会话中尝试获取的UserToken [{}]", userToken);
        AuthnFactorInstance instance = AuthnFactorInstanceUtils.getFactorInstance(instanceId);
        CassUpSsoProperties properties = CassUpSsoProperties.parse(instance);
        // 获取岗位
        if (StringUtils.isEmpty(userToken)){
            userToken = getUserToken(properties, session,token);
        }
        if (StringUtils.isNotEmpty(userToken)){
            // 配置应用时需要指定为：http://szsk.sky/sky-portal/#/subsys?sysurl=https://应用地址+应用端口
            // sysurl= 实际的子应用地址，需要这么配置资源
            // 访问子应用是
            String ssoParam = "userToken=" + userToken + "&from=prod&logname=" + user
                    +"&cookie="+ Base64.encode("SESSION="+session);
            log.info("buildExtUrlParam 输出最终返回SSO地址为： [{}] ", ssoParam);
            return ssoParam;
        }
        return super.buildExtUrlParam(json);
    }

    /**
     * 获取userToken
     * @return
     */
    public String getUserToken(CassUpSsoProperties properties, String session, String token) {
        try {
            log.info("getUserToken（） 传递的参数 ： session: [{}] , token: [{}]", session , token);
            String character = HttpRequest.get(properties.getForCharacterUrl())
                    .header("Cookie", "SESSION= " + session)
                    .header("Accept", "application/json, text/plain, */*")
                    .timeout(7000)
                    .execute().body();
            log.info("获取岗位数据为：[{}]", character);
            if (StringUtils.isNotEmpty(character)){
                JSONObject cJson = JSONObject.parseObject(character);
                Integer status = cJson.getInteger("status");
                if (status != 0){
                    throw new CodedException(IErrorCodeBase.SSO_CODE_EXCHANGE_TOKEN_FAIL, token);
                }
                // 岗位信息
                JSONArray data = cJson.getJSONArray("data");
                if (data.isEmpty()){
                    throw new CodedException(IErrorCodeBase.SSO_CODE_EXCHANGE_TOKEN_FAIL, "获取岗位信息为空");
                }
                String charId = data.getJSONObject(0).getString("charId");
                log.info("getUserToken发送的数据有：charId:[{}] , session:[{}] , token : [{}]", charId, session ,token);
                // 获取userToken
                HashMap<String, Object> postTokenParam = new HashMap<>();
                postTokenParam.put("username",  charId);
                String userTokenBody = HttpRequest.post(properties.getForUserTokenUrl())
                        .header("Cookie", "SESSION= " + session)
                        // 其中X-CSRF-TOKEN值是登录接口返回的token值
                        .header("X-CSRF-TOKEN", token)
                        .header("Accept", "application/json")
                        .form(postTokenParam)
                        .timeout(9000)
                        .execute().body();
                log.info("获取userToken 返回值为：[{}]", userTokenBody);
                if (StringUtils.isNotEmpty(character)){
                    JSONObject tokenJson = JSONObject.parseObject(userTokenBody);
                    Integer tokenStatus = tokenJson.getInteger("status");
                    if (tokenStatus != 0){
                        throw new CodedException(IErrorCodeBase.SSO_CODE_EXCHANGE_TOKEN_FAIL, token);
                    }
                    // 返回最终token
                    String userToken = tokenJson.getJSONObject("data").getString("userToken");
                    AuthnContext context = contextService.getAuthnContextByRequest();
                    context.setAttribute(CassCommon.USER_TOKEN, userToken);
                    return userToken;
                }
            }else{
                throw new CodedException(IErrorCodeBase.SSO_CODE_EXCHANGE_TOKEN_FAIL, token);
            }
        } catch (RestClientException e) {
            if (e instanceof HttpClientErrorException && ((HttpClientErrorException) e).getRawStatusCode() == 401) {
                log.error("token过期/错误的token，token = {}", token);
                userLogOut();
                throw new CodedException(IErrorCodeBase.UN_AUTHENCATED);
            } else {
                log.error("请求code失败，token = {}", token);
                throw new CodedException(IErrorCodeBase.SSO_CODE_EXCHANGE_TOKEN_FAIL, token);
            }
        }
        throw new CodedException(IErrorCodeBase.SSO_CODE_EXCHANGE_TOKEN_FAIL, token);
    }



    private void userLogOut() {
        String sessionId = SecurityContextHolder.getUserSessionId();
        String uuid = sessionAdapterFactory.getAuthnAdatper().getUserSession(sessionId).getUuid();
        HashMap<String, String> reason = new HashMap<>();
        reason.put("zh", "token过期");
        sessionAdapterFactory.getAuthnAdatper().offlineSession(uuid, SSOProtocol.SAML, reason);
    }

    @Override
    public String logout(HttpServletRequest request, HttpServletResponse response, AuthnContext authnContext) throws CodedException {
        cache.remove(authnContext.getId());
        return super.logout(request, response, authnContext);
    }
}
